using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Management.Instrumentation;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Threading;
using System.Xml;
using System.Xml.XPath;

using Microsoft.XLANGs.BaseTypes;

namespace TKH.Biztalk.Diagnostics
{
    /// <summary>
    /// Biztalk Event class used for logging and tracing
    /// Borrowed from Enterprise Library LogEvent class and modified.
    /// </summary>
    [InstrumentationClass(InstrumentationType.Event)]
    [ManagedName("TKH_BiztalkEvent")]
    public class BiztalkEvent : ICloneable, IXPathNavigable
    {
        #region fields
        private TraceEventType severity = TraceEventType.Information;
        private string description = string.Empty;
        private int eventId = 0;
        private Guid activityId;
        private Guid? relatedActivityId;
        private IList<string> categories = new List<string>(0);
        private DateTime timeStamp = DateTime.MaxValue;
        private string machineName = string.Empty;
        private string appDomainName;
        private string processId;
        private string processName;
        private string threadName;
        private string win32ThreadId;
        private string assemblyName;

        //Biztalk specific data
        private string serviceId;
        private string serviceName;
        private string instanceId;
        
        private IList<object> extendedProperties;
        private XmlDocument xmlDocument;
        private string errorString = string.Empty;
        
        #endregion

        #region properties
        /// <summary>
        /// Log entry severity as a <see cref="Severity"/> enumeration. (Unspecified, Information, Warning or Error).
        /// </summary>
        [IgnoreMember]
        public TraceEventType Severity
        {
            get { return this.severity; }
            set { this.severity = value; }
        }

        /// <summary>
        /// Message body to log.  Value from ToString() method from message object.
        /// </summary>
        public string Description
        {
            get { return this.description; }
            set { this.description = value; }
        }

        /// <summary>
        /// Event number or identifier.
        /// </summary>
        public int EventId
        {
            get { return this.eventId; }
            set { this.eventId = value; }
        }

        /// <summary>
        /// Tracing activity id
        /// </summary>
        [IgnoreMember]
        public Guid ActivityId
        {
            get { return this.activityId; }
            set { this.activityId = value; }
        }

        /// <summary>
        /// Related activity id
        /// </summary>
        [IgnoreMember]
        public Guid? RelatedActivityId
        {
            get { return this.relatedActivityId; }
            set { this.relatedActivityId = value; }
        }

        /// <summary>
        /// Category name used to route the log entry to a one or more trace listeners.
        /// </summary>
        [IgnoreMember]
        public IList<string> Categories
        {
            get { return categories; }
            set { this.categories = value; }
        }

        /// <summary>
        /// Event categories published in WMI Event
        /// </summary>
        public string EventCategories
        {
            get 
            {
                string result = string.Empty;
                for (int i = 0; i < categories.Count; i++ )
                {
                    result += categories[i];
                    result += i != (categories.Count - 1) ? ", " : "";
                }
                return result;
            }
        }

        /// <summary>
        /// Date and time of the log entry message.
        /// </summary>
        public DateTime TimeStamp
        {
            get { return this.timeStamp; }
            set { this.timeStamp = value; }
        }

        /// <summary>
        /// Name of the computer.
        /// </summary>
        public string MachineName
        {
            get { return this.machineName; }
            set { this.machineName = value; }
        }

        /// <summary>
        /// The <see cref="AppDomain"/> in which the program is running
        /// </summary>
        public string AppDomainName
        {
            get { return this.appDomainName; }
            set { this.appDomainName = value; }
        }

        /// <summary>
        /// The Win32 process ID for the current running process.
        /// </summary>
        public string ProcessId
        {
            get { return processId; }
            set { processId = value; }
        }

        /// <summary>
        /// The name of the current running process.
        /// </summary>
        public string ProcessName
        {
            get { return processName; }
            set { processName = value; }
        }

        /// <summary>
        /// The name of the .NET thread.
        /// </summary>
        ///  <seealso cref="Win32ThreadId"/>
        public string ManagedThreadName
        {
            get { return threadName; }
            set { threadName = value; }
        }

        /// <summary>
        /// The Win32 Thread ID for the current thread.
        /// </summary>
        public string Win32ThreadId
        {
            get { return win32ThreadId; }
            set { win32ThreadId = value; }
        }

        /// <summary>
        /// The Win32 Thread ID for the current thread.
        /// </summary>
        public string AssemblyName
        {
            get { return assemblyName; }
            set { assemblyName = value; }
        }

        /// <summary>
        /// Biztalk Service Identifier
        /// </summary>
        public string ServiceId
        {
            get { return serviceId; }
            set { serviceId = value; }
        }

        /// <summary>
        /// Biztalk Service Identifier
        /// </summary>
        public string ServiceName
        {
            get { return serviceName; }
            set { serviceName = value; }
        }
        /// <summary>
        /// Biztalk orchestration Instance Id
        /// </summary>
        public string InstanceId
        {
            get { return instanceId; }
            set { instanceId = value; }
        }
      
        /// <summary>
        /// Dictionary of key/value pairs to record.
        /// </summary>
        [IgnoreMember]
        public IList<object> ExtendedProperties
        {
            get
            {
                if (extendedProperties == null)
                {
                    extendedProperties = new List<object>();
                }
                return this.extendedProperties;
            }
            set { this.extendedProperties = value; }
        }
        /// <summary>
        /// Extended data published in WMI Event
        /// </summary>
        public string ExtendedData
        {
            get
            {
                string result = string.Empty;
                for (int i = 0; i < extendedProperties.Count; i++)
                {
                    result += extendedProperties[i];
                    result += i != (extendedProperties.Count - 1) ? ", " : "";
                }
                return result;
            }
        }
       
        #endregion

        #region constructors
        /// <summary>
        /// Initialize a new instance of a <see cref="BiztalkEvent"/> class.
        /// </summary>
        public BiztalkEvent()
        {
            CollectIntrinsicProperties();
        }

        /// <summary>
        /// Create a new instance of <see cref="BiztalkEvent"/> with a full set of constructor parameters
        /// </summary>
        /// <param name="severity">Log entry severity as a <see cref="Severity"/> enumeration. (Unspecified, Information, Warning or Error).</param>
        /// <param name="category">Category name used to route the log entry to a one or more trace listeners.</param>
        /// <param name="eventId">Event number or identifier.</param>
        /// <param name="description">Additional description of the log entry message.</param>
        /// <param name="serviceId">Biztalk orchestration service id</param>
        /// <param name="serviceName">Biztalk orchestration service name</param>
        /// <param name="properties">Optional data objects to record.</param>
        public BiztalkEvent(TraceEventType severity, string category, int eventId, string description, 
                         Guid serviceId, string serviceName, params object[] properties)
            : this(severity, BuildCategoriesCollection(category), eventId, description, serviceId, serviceName, properties)
        {
        }

        /// <summary>
        /// Create a new instance of <see cref="BiztalkEvent"/> with a full set of constructor parameters
        /// </summary>
        /// <param name="severity">Log entry severity as a <see cref="Severity"/> enumeration. (Unspecified, Information, Warning or Error).</param>
        /// <param name="categories">List of category names used to route the log entry to a one or more trace listeners.</param>
        /// <param name="eventId">Event number or identifier.</param>
        /// <param name="description">Additional description of the log entry message.</param>
        /// <param name="serviceId">Biztalk orchestration service id</param>
        /// <param name="serviceName">Biztalk orchestration service name</param>
        /// <param name="properties">Optional data objects to record.</param>
        public BiztalkEvent(TraceEventType severity, IList<string> categories, int eventId, string description, 
                         Guid serviceId, string serviceName, params object[] properties)
        {
            CollectIntrinsicProperties();

            this.description = description;
            this.eventId = eventId;
            this.severity = severity;
            this.serviceId = serviceId.ToString();
            this.serviceName = serviceName;
            this.instanceId = GetActivityId().ToString();
            this.categories = categories;
            this.extendedProperties = new List<object>(properties);
        }


        #endregion

        #region IXPathNavigable Members
        /// <summary>
        /// Used by XmlTraceListener to create <see cref="System.Xml.XPath.XPathNavigator"/>
        /// </summary>
        /// <returns></returns>

        public XPathNavigator CreateNavigator()
        {
            xmlDocument = new XmlDocument();
            XPathNavigator navigator = xmlDocument.CreateNavigator();
            XmlWriter xw = navigator.AppendChild();
            xw.WriteStartElement(TraceRecordConstants.RootName, TraceRecordConstants.Namespace);
            xw.WriteElementString("TraceIdentifier", TraceRecordConstants.Namespace, "http://tkh.co.nz/TKH.Diagnostics.TraceActivity.aspx");
            xw.WriteElementString("Description", TraceRecordConstants.Namespace, this.Description);
            xw.WriteElementString("AppDomain", TraceRecordConstants.Namespace, AppDomain.CurrentDomain.FriendlyName);
            xw.WriteStartElement(TraceRecordConstants.ExtendedDataName, TraceRecordConstants.ExtendedDataNamespace);
            foreach (object property in ExtendedProperties)
            {
                if (property is Activity)
                {
                    xw.WriteElementString("ActivityName", ((Activity)property).ActivityName);
                    xw.WriteElementString("ActivityType", ((Activity)property).ActivityType);
                }
                else if (property is Message)
                {
                    Message message = (Message)property;
                    xw.WriteStartElement("Message", null);
                    xw.WriteAttributeString("type", message.MessageType);
                    xw.WriteNode(message.CreateNavigator(), true);
                    xw.WriteEndElement();
                }
                else if (property is Exception)
                {
                    Exception exception = (Exception)property;
                    while (exception != null)
                    {
                        xw.WriteStartElement("exception", null);
                        xw.WriteAttributeString("type", exception.GetType().ToString());
                        xw.WriteElementString("source", exception.Source);
                        xw.WriteElementString("message", exception.Message);
                        xw.WriteElementString("stack", exception.StackTrace);
                        xw.WriteStartElement("data", null);
                        foreach (DictionaryEntry item in exception.Data)
                        {
                            xw.WriteElementString((string)item.Key.ToString(), (string)item.Value.ToString());
                        }
                        xw.WriteEndElement();
                        exception = exception.InnerException;
                    }
                }                
                else
                {
                    xw.WriteElementString("data", property.ToString());
                }
            }
            xw.WriteEndElement();
            xw.WriteEndElement();
            xw.Close();
            return navigator;
        }

        #endregion

        /// <summary>
        /// Returns a <see cref="String"/> that represents the current <see cref="BiztalkEvent"/>, 
        /// using a default formatting template. Used by the EventLogListener
        /// </summary>
        /// <returns>A <see cref="String"/> that represents the current <see cref="BiztalkEvent"/></returns>
        public override string ToString()
        {
            StringBuilder builder = new StringBuilder("Biztalk Event:\n");
            builder.AppendFormat("Severity: {0}\n", this.severity.ToString());
            builder.AppendFormat("Description: {0}\n", this.description);
            builder.AppendFormat("EventId: {0}\n", this.eventId.ToString());
            builder.AppendFormat("ActivityId: {0}\n", this.activityId.ToString());
            builder.AppendFormat("RelatedActivityId: {0}\n", this.relatedActivityId.ToString());
            builder.AppendFormat("Timestamp: {0}\n", this.timeStamp.ToString());
            builder.AppendFormat("MachineName: {0}\n", this.machineName);
            builder.AppendFormat("AppDomainName: {0}\n", this.appDomainName);
            builder.AppendFormat("ProcessId: {0}\n", this.processId);
            builder.AppendFormat("ProcessName: {0}\n", this.processName);
            builder.AppendFormat("ManagedThreadname: {0}\n", this.threadName);
            builder.AppendFormat("Win32ThreadId: {0}\n", this.win32ThreadId);
            builder.AppendFormat("AssemblyName: {0}\n", this.assemblyName);
            builder.Append("----------------------------------------------------\n");
            builder.Append("Biztalk properties:\n");
            builder.AppendFormat("ServiceId: {0}\n", this.serviceId.ToString());
            builder.AppendFormat("ServiceName: {0}\n", this.serviceName);
            builder.AppendFormat("InstanceId: {0}\n", this.instanceId);
            builder.Append("----------------------------------------------------\n");
            foreach (object property in ExtendedProperties)
            {
                if (property is Activity)
                {
                    Activity activity = (Activity)property;
                    builder.AppendFormat("{0}\n", activity.ToString());
                }
                else if (property is Message)
                {
                    Message message = (Message)property;
                    builder.AppendFormat("{0}\n", message.ToString());
                }
                else if (property is Exception)
                {
                    Exception exception = (Exception)property;
                    while (exception != null)
                    {
                        builder.Append("\nException\n");
                        builder.AppendFormat("type: {0}\n", exception.GetType().ToString());
                        builder.AppendFormat("source: {0}\n", exception.Source);
                        builder.AppendFormat("message: {0}\n", exception.Message);
                        builder.AppendFormat("stack: {0}\n", exception.StackTrace);
                        builder.Append("data\n");
                        foreach (DictionaryEntry item in exception.Data)
                        {
                            builder.AppendLine(item.Key.ToString());
                            builder.AppendLine(item.Value.ToString());
                        }
                        exception = exception.InnerException;
                    }
                }
                else
                {
                    builder.AppendFormat("data\n {0}\n", property.ToString());
                }

            }
            return builder.ToString();
        }

        /// <summary>
        /// Creates a new <see cref="BiztalkEvent"/> that is a copy of the current instance.
        /// </summary>
        /// <remarks>
        /// If the dictionary contained in <see cref="ExtendedProperties"/> implements <see cref="ICloneable"/>, the resulting
        /// <see cref="BiztalkEvent"/> will have its ExtendedProperties set by calling <c>Clone()</c>. Otherwise the resulting
        /// <see cref="BiztalkEvent"/> will have its ExtendedProperties set to <see langword="null"/>.
        /// </remarks>
        /// <implements>ICloneable.Clone</implements>
        /// <returns>A new <c>LogEntry</c> that is a copy of the current instance.</returns>
        public object Clone()
        {
            BiztalkEvent result = new BiztalkEvent();
            
            result.Severity = this.Severity;
            result.Description = this.description;
            result.EventId = this.eventId;
            result.ActivityId = this.ActivityId;
            result.RelatedActivityId = this.relatedActivityId;

            result.Categories = new List<string>(this.Categories);
            
            result.TimeStamp = this.timeStamp;
            result.MachineName = this.machineName;
            result.AppDomainName = this.appDomainName;
            result.ProcessId = this.processId;
            result.ProcessName = this.processName;
            result.ManagedThreadName = this.threadName;
            result.AssemblyName = this.assemblyName;

            result.ServiceId = this.serviceId;

            // clone categories
            

            // clone extended properties
            if (this.extendedProperties != null)
                result.ExtendedProperties = new List<object>(this.extendedProperties);

            return result;
        }

        /// <summary>
        /// Set the intrinsic properties such as MachineName and UserIdentity.
        /// </summary>
        private void CollectIntrinsicProperties()
        {
            this.TimeStamp = DateTime.UtcNow;

            try
            {
                this.ActivityId = GetActivityId();
            }
            catch (Exception)
            {
                this.ActivityId = Guid.Empty;
            }

            // do not try to avoid the security exception, as it would only duplicate the stack walk
            try
            {
                MachineName = Environment.MachineName;
            }
            catch (Exception e)
            {
                this.MachineName = String.Format(Properties.Resources.IntrinsicPropertyError, e.Message);
            }

            try
            {
                appDomainName = AppDomain.CurrentDomain.FriendlyName;
            }
            catch (Exception e)
            {
                appDomainName = String.Format(Properties.Resources.IntrinsicPropertyError, e.Message);
            }

            // check whether the unmanaged code permission is available to avoid three potential stack walks
            bool unmanagedCodePermissionAvailable = false;
            SecurityPermission unmanagedCodePermission = new SecurityPermission(SecurityPermissionFlag.UnmanagedCode);
            // avoid a stack walk by checking for the permission on the current assembly. this is safe because there are no
            // stack walk modifiers before the call.
            if (SecurityManager.IsGranted(unmanagedCodePermission))
            {
                try
                {
                    unmanagedCodePermission.Demand();
                    unmanagedCodePermissionAvailable = true;
                }
                catch (SecurityException)
                { }
            }

            if (unmanagedCodePermissionAvailable)
            {
                try
                {
                    processId = GetCurrentProcessId();
                }
                catch (Exception e)
                {
                    processId = String.Format(Properties.Resources.Culture, Properties.Resources.IntrinsicPropertyError, e.Message);
                }

                try
                {
                    processName = GetProcessName();
                }
                catch (Exception e)
                {
                    processName = String.Format(Properties.Resources.Culture, Properties.Resources.IntrinsicPropertyError, e.Message);
                }

                try
                {
                    win32ThreadId = GetCurrentThreadId();
                }
                catch (Exception e)
                {
                    win32ThreadId = String.Format(Properties.Resources.Culture, Properties.Resources.IntrinsicPropertyError, e.Message);
                }
            }
            else
            {
                processId = String.Format(Properties.Resources.IntrinsicPropertyError,
                    Properties.Resources.NoUnmanagedCodePermissionError);
                processName = String.Format(Properties.Resources.Culture,
                    Properties.Resources.IntrinsicPropertyError,
                    Properties.Resources.NoUnmanagedCodePermissionError);
                win32ThreadId = String.Format(Properties.Resources.Culture,
                    Properties.Resources.IntrinsicPropertyError,
                    Properties.Resources.NoUnmanagedCodePermissionError);
            }

            try
            {
                threadName = Thread.CurrentThread.Name;
            }
            catch (Exception e)
            {
                threadName = String.Format(Properties.Resources.Culture, Properties.Resources.IntrinsicPropertyError, e.Message);
            }
        }

        /// <summary>
        /// Gets the current process name.
        /// </summary>
        /// <returns>The process name.</returns>
        public static string GetProcessName()
        {
            StringBuilder buffer = new StringBuilder(1024);
            int length = NativeMethods.GetModuleFileName(NativeMethods.GetModuleHandle(null), buffer, buffer.Capacity);
            return buffer.ToString();
        }

        private static IList<string> BuildCategoriesCollection(string category)
        {
            if (string.IsNullOrEmpty(category))
                throw new ArgumentNullException("category");

            return new List<string>(new string[] { category });
        }

        private static Guid GetActivityId()
        {
            return System.Diagnostics.Trace.CorrelationManager.ActivityId;
        }

        private static string GetCurrentProcessId()
        {
            return NativeMethods.GetCurrentProcessId().ToString();
        }

        private static string GetCurrentThreadId()
        {
            return NativeMethods.GetCurrentThreadId().ToString();
        }

       
    }
}
